#include "common/pch.h"
#include "mmap/mmapmgr.h"

#include <string.h>

#include <format>

#include <cftf/loger/syslog.h>

#include "DetourAlloc.h"
#include "DetourNavMesh.h"
#include "DetourNavMeshQuery.h"

#include "object/unit.h"
#include "object/player.h"
#include "object/creature.h"
#include "metadata/metamgr.h"
#include "metadata/creature.h"
#include "metadata/creatureinfo.h"
#include "world/world.h"

#if 0
#define MMAP_MAGIC 0x4d4d4150   // 'MMAP'
#define MMAP_VERSION 4

struct MmapTileHeader
{
    unsigned int mmapMagic;
    unsigned int dtVersion;
    unsigned int mmapVersion;
    unsigned int size;
    bool usesLiquids : 1;

    MmapTileHeader() : mmapMagic(MMAP_MAGIC), dtVersion(DT_NAVMESH_VERSION),
        mmapVersion(MMAP_VERSION), size(0), usesLiquids(true) {
    }
};
#endif
//  memory management
void* dtCustomAlloc(size_t size, dtAllocHint /*hint*/)
{
    return (void*)new unsigned char[size];
}

void dtCustomFree(void* ptr)
{
    delete[](unsigned char*)ptr;
}

namespace mmap
{

    // dummy struct to hold map's mmap data
    class MMapData
    {
    public:
        MMapData(dtNavMesh* mesh) : nav_mesh(mesh) {}
        ~MMapData()
        {
            for (auto i = navmesh_queries.begin(); i != navmesh_queries.end(); ++i) {
                dtFreeNavMeshQuery(i->second);
            }

            if (nav_mesh) {
                dtFreeNavMesh(nav_mesh);
            }
        }

        dtNavMesh* nav_mesh = nullptr;

        // we have to use single dtNavMeshQuery for every instance, since those are not thread safe
        std::unordered_map<unsigned int, dtNavMeshQuery*> navmesh_queries;     // instanceId to query
        std::unordered_map<unsigned int, dtTileRef> mmap_loaded_tiles;        // maps [map grid coords] to [dtTile]
    };

    void MMapMgr::PreventPathfindingOnMaps(const char* ignoreMapIds)
    {
        if (!disabled_ids_) {
            disabled_ids_ = std::make_shared<std::set<unsigned int>>();
        }

        unsigned int strLenght = (unsigned int)strlen(ignoreMapIds) + 1;
        char* mapList = new char[strLenght];
        memcpy(mapList, ignoreMapIds, sizeof(char) * strLenght);

        char* idstr = (char*)strtok(mapList, ",");
        while (idstr) {
            disabled_ids_->insert(unsigned int(atoi(idstr)));
            idstr = (char*)strtok(NULL, ",");
        }

        delete[] mapList;
    }

    bool MMapMgr::IsPathfindingEnabled(unsigned int mapId, const battle::object::Unit* unit)
    {
        if (!mt::MetaMgr::Instance().GetConf()->At("MangosdConf.mmap.enabled")->AsXValue().Get<int>()) {
            return false;
        }

        if (unit) {
            // always use mmaps for players
            if (unit->GetTypeId() == TYPEID_PLAYER) {
                return true;
            }

            if (IsPathfindingForceDisabled(unit)) {
                return false;
            }

            if (IsPathfindingForceEnabled(unit)) {
                return true;
            }

            // always use mmaps for pets of players (can still be disabled by extra-flag for pet creature)
            if (unit->GetTypeId() == TYPEID_UNIT && ((battle::object::Creature*)unit)->IsPet() && unit->GetOwner() &&
                unit->GetOwner()->GetTypeId() == TYPEID_PLAYER) {
                return true;
            }
        }

        return disabled_ids_->find(mapId) == disabled_ids_->end();
    }

    void MMapMgr::Clear()
    {
        disabled_ids_ = nullptr;
    }

    bool MMapMgr::IsPathfindingForceEnabled(const battle::object::Unit* unit)
    {
        if (const battle::object::Creature* pCreature = dynamic_cast<const battle::object::Creature*>(unit)) {
            if (auto pInfo = pCreature->GetCreatureInfo()) {
                if (pInfo->ExtraFlags & CREATURE_FLAG_EXTRA_MMAP_FORCE_ENABLE) {
                    return true;
                }
            }
        }
        return false;
    }

    bool MMapMgr::IsPathfindingForceDisabled(const battle::object::Unit* unit)
    {
        if (auto pCreature = dynamic_cast<const battle::object::Creature*>(unit)) {
            if (auto pInfo = pCreature->GetCreatureInfo()) {
                if (pInfo->ExtraFlags & CREATURE_FLAG_EXTRA_MMAP_FORCE_DISABLE) {
                    return true;
                }
            }
        }
        return false;
    }

    MMapMgr::~MMapMgr()
    {
        for (auto i = loaded_mmaps_.begin(); i != loaded_mmaps_.end(); ++i) {
            delete i->second;
        }

        // by now we should not have maps loaded
        // if we had, tiles in MMapData->mmapLoadedTiles, their actual data is lost!
    }

    bool MMapMgr::LoadMapData(unsigned int mapId)
    {
        // we already have this map loaded?
        if (loaded_mmaps_.find(mapId) != loaded_mmaps_.end()) {
            return true;
        }

        // load and init dtNavMesh - read parameters from file
        unsigned int pathLen = (unsigned int)(World::Instance().GetDataPath().length() + strlen("mmaps/%03i.mmap") + 1);
        char* fileName = new char[pathLen];
        snprintf(fileName, pathLen, (World::Instance().GetDataPath() + "mmaps/%03i.mmap").c_str(), mapId);

        FILE* file = fopen(fileName, "rb");
        if (!file) {
            if (IsPathfindingEnabled(mapId)) {
                cftf::loger::SysLog::Instance().Error(std::format("MMAP:loadMapData: Error: Could not open mmap file '{}'", fileName));
            }
            delete[] fileName;
            return false;
        }

        dtNavMeshParams params;
        size_t file_read = fread(&params, sizeof(dtNavMeshParams), 1, file);
        if (file_read <= 0) {
            cftf::loger::SysLog::Instance().Error(std::format("MMAP:loadMapData: Failed to load mmap {:03} from file {}", mapId, fileName));
            delete[] fileName;
            fclose(file);
            return false;
        }
        fclose(file);

        dtNavMesh* mesh = dtAllocNavMesh();
        CFT_ASSERT(mesh);
        dtStatus dtResult = mesh->init(&params);
        if (dtStatusFailed(dtResult)) {
            dtFreeNavMesh(mesh);
            cftf::loger::SysLog::Instance().Error(std::format("MMAP:loadMapData: Failed to initialize dtNavMesh for mmap {:03} from file {}", mapId, fileName));
            delete[] fileName;
            return false;
        }

        delete[] fileName;

        cftf::loger::SysLog::Instance().Deubg(std::format("MMAP:loadMapData: Loaded {:03}.mmap", mapId));

        // store inside our map list
        MMapData* mmap_data = new MMapData(mesh);
        mmap_data->mmap_loaded_tiles.clear();

        loaded_mmaps_.insert(std::pair<unsigned int, MMapData*>(mapId, mmap_data));
        return true;
    }

    unsigned int MMapMgr::PackTileID(int x, int y)
    {
        return unsigned int(x << 16 | y);
    }

    bool MMapMgr::LoadMap(unsigned int mapId, int x, int y)
    {
        // make sure the mmap is loaded and ready to load tiles
        if (!LoadMapData(mapId)) {
            return false;
        }

        // get this mmap data
        MMapData* mmap = loaded_mmaps_[mapId];
        CFT_ASSERT(mmap->nav_mesh);

        // check if we already have this tile loaded
        unsigned int packedGridPos = PackTileID(x, y);
        if (mmap->mmap_loaded_tiles.find(packedGridPos) != mmap->mmap_loaded_tiles.end()) {
            cftf::loger::SysLog::Instance().Error(std::format("MMAP:loadMap: Asked to load already loaded navmesh tile. {:03}{:02}{:02}.mmtile", mapId, x, y));
            return false;
        }

        // load this tile :: mmaps/MMMXXYY.mmtile
        unsigned int pathLen = (unsigned int)(World::Instance().GetDataPath().length() + strlen("mmaps/%03i%02i%02i.mmtile") + 1);
        char* fileName = new char[pathLen];
        snprintf(fileName, pathLen, (World::Instance().GetDataPath() + "mmaps/%03i%02i%02i.mmtile").c_str(), mapId, x, y);

        FILE* file = fopen(fileName, "rb");
        if (!file) {
            cftf::loger::SysLog::Instance().Error(std::format("ERROR: MMAP:loadMap: Could not open mmtile file '{}'", fileName));
            delete[] fileName;
            return false;
        }
        delete[] fileName;

        // read header
        MmapTileHeader fileHeader;
        size_t file_read = fread(&fileHeader, sizeof(MmapTileHeader), 1, file);

        if (file_read <= 0) {
            cftf::loger::SysLog::Instance().Error(std::format("MMAP:loadMap: Could not load mmap {:03}{:02}{:02}.mmtile", mapId, x, y));
            fclose(file);
            return false;
        }

        if (fileHeader.mmapMagic != MMAP_MAGIC) {
            cftf::loger::SysLog::Instance().Error(std::format("MMAP:loadMap: Bad header in mmap {:03}{:02}{%02}.mmtile", mapId, x, y));
            fclose(file);
            return false;
        }

        if (fileHeader.mmapVersion != MMAP_VERSION) {
            cftf::loger::SysLog::Instance().Error(std::format("MMAP:loadMap: {:03}{:02}{:02}.mmtile was built with generator v{}, expected v{}",
                mapId, x, y, fileHeader.mmapVersion, MMAP_VERSION));
            fclose(file);
            return false;
        }

        unsigned char* data = (unsigned char*)dtAlloc(fileHeader.size, DT_ALLOC_PERM);
        CFT_ASSERT(data);

        size_t result = fread(data, fileHeader.size, 1, file);
        if (!result) {
            cftf::loger::SysLog::Instance().Error(std::format("MMAP:loadMap: Bad header or data in mmap {:03}{:02}{:02}.mmtile", mapId, x, y));
            fclose(file);
            return false;
        }

        fclose(file);

        dtMeshHeader* header = (dtMeshHeader*)data;
        dtTileRef tileRef = 0;

        // memory allocated for data is now managed by detour, and will be deallocated when the tile is removed
        dtStatus dtResult = mmap->nav_mesh->addTile(data, fileHeader.size, DT_TILE_FREE_DATA, 0, &tileRef);
        if (dtStatusFailed(dtResult)) {
            cftf::loger::SysLog::Instance().Error(std::format("MMAP:loadMap: Could not load {:03}{:02}{:02}.mmtile into navmesh", mapId, x, y));
            dtFree(data);
            return false;
        }

        mmap->mmap_loaded_tiles.insert(std::pair<unsigned int, dtTileRef>(packedGridPos, tileRef));
        ++loaded_tiles_;
        cftf::loger::SysLog::Instance().Deubg(std::format("MMAP:loadMap: Loaded mmtile {:03}[{:02},{:02}] into {:03}[{:02},{:02}]", mapId, x, y, mapId, header->x, header->y));
        return true;
    }

    bool MMapMgr::UnloadMap(unsigned int mapId, int x, int y)
    {
        // check if we have this map loaded
        if (loaded_mmaps_.find(mapId) == loaded_mmaps_.end()) {
            // file may not exist, therefore not loaded
            cftf::loger::SysLog::Instance().Deubg(std::format("MMAP:unloadMap: Asked to unload not loaded navmesh map. {:03}{:02}{:02}.mmtile", mapId, x, y));
            return false;
        }

        MMapData* mmap = loaded_mmaps_[mapId];

        // check if we have this tile loaded
        unsigned int packedGridPos = PackTileID(x, y);
        if (mmap->mmap_loaded_tiles.find(packedGridPos) == mmap->mmap_loaded_tiles.end()) {
            // file may not exist, therefore not loaded
            cftf::loger::SysLog::Instance().Deubg(std::format("MMAP:unloadMap: Asked to unload not loaded navmesh tile. {:03}{:02}{:02}.mmtile", mapId, x, y));
            return false;
        }

        dtTileRef tileRef = mmap->mmap_loaded_tiles[packedGridPos];

        // unload, and mark as non loaded
        dtStatus dtResult = mmap->nav_mesh->removeTile(tileRef, NULL, NULL);
        if (dtStatusFailed(dtResult)) {
            // this is technically a memory leak
            // if the grid is later reloaded, dtNavMesh::addTile will return error but no extra memory is used
            // we can not recover from this error - assert out
            cftf::loger::SysLog::Instance().Error(std::format("MMAP:unloadMap: Could not unload {:03}{:02}{:02}.mmtile from navmesh", mapId, x, y));
            CFT_ASSERT(false);
        } else {
            mmap->mmap_loaded_tiles.erase(packedGridPos);
            --loaded_tiles_;
            cftf::loger::SysLog::Instance().Deubg(std::format("MMAP:unloadMap: Unloaded mmtile {:03}[{:02},{:02}] from {:03}", mapId, x, y, mapId));
            return true;
        }

        return false;
    }

    bool MMapMgr::UnloadMap(unsigned int mapId)
    {
        if (loaded_mmaps_.find(mapId) == loaded_mmaps_.end()) {
            // file may not exist, therefore not loaded
            cftf::loger::SysLog::Instance().Deubg(std::format("MMAP:unloadMap: Asked to unload not loaded navmesh map {:03}", mapId));
            return false;
        }

        // unload all tiles from given map
        MMapData* mmap = loaded_mmaps_[mapId];
        for (auto i = mmap->mmap_loaded_tiles.begin(); i != mmap->mmap_loaded_tiles.end(); ++i) {
            unsigned int x = (i->first >> 16);
            unsigned int y = (i->first & 0x0000FFFF);
            dtStatus dtResult = mmap->nav_mesh->removeTile(i->second, NULL, NULL);
            if (dtStatusFailed(dtResult)) {
                cftf::loger::SysLog::Instance().Error(std::format("MMAP:unloadMap: Could not unload {:03}{:02}{:02}.mmtile from navmesh", mapId, x, y));
            } else {
                --loaded_tiles_;
                cftf::loger::SysLog::Instance().Deubg(std::format("MMAP:unloadMap: Unloaded mmtile {:03}[{:02},{:02}] from {:03}", mapId, x, y, mapId));
            }
        }

        delete mmap;
        loaded_mmaps_.erase(mapId);
        cftf::loger::SysLog::Instance().Deubg(std::format("MMAP:unloadMap: Unloaded {:03}.mmap", mapId));

        return true;
    }

    bool MMapMgr::UnloadMapInstance(unsigned int mapId, unsigned int instanceId)
    {
        // check if we have this map loaded
        if (loaded_mmaps_.find(mapId) == loaded_mmaps_.end()) {
            // file may not exist, therefore not loaded
            cftf::loger::SysLog::Instance().Deubg(std::format("MMAP:unloadMapInstance: Asked to unload not loaded navmesh map {:03}", mapId));
            return false;
        }

        MMapData* mmap = loaded_mmaps_[mapId];
        if (mmap->navmesh_queries.find(instanceId) == mmap->navmesh_queries.end()) {
            cftf::loger::SysLog::Instance().Deubg(std::format("MMAP:unloadMapInstance: Asked to unload not loaded dtNavMeshQuery mapId {:03} instanceId {}", mapId, instanceId));
            return false;
        }

        dtNavMeshQuery* query = mmap->navmesh_queries[instanceId];

        dtFreeNavMeshQuery(query);
        mmap->navmesh_queries.erase(instanceId);
        cftf::loger::SysLog::Instance().Deubg(std::format("MMAP:unloadMapInstance: Unloaded mapId {:03} instanceId {}", mapId, instanceId));

        return true;
    }

    dtNavMesh const* MMapMgr::GetNavMesh(unsigned int mapId)
    {
        if (loaded_mmaps_.find(mapId) == loaded_mmaps_.end()) {
            return nullptr;
        }

        return loaded_mmaps_[mapId]->nav_mesh;
    }

    dtNavMeshQuery const* MMapMgr::GetNavMeshQuery(unsigned int mapId, unsigned int instanceId)
    {
        if (loaded_mmaps_.find(mapId) == loaded_mmaps_.end()) {
            return nullptr;
        }

        MMapData* mmap = loaded_mmaps_[mapId];
        if (mmap->navmesh_queries.find(instanceId) == mmap->navmesh_queries.end()) {
            // allocate mesh query
            dtNavMeshQuery* query = dtAllocNavMeshQuery();
            CFT_ASSERT(query);
            dtStatus dtResult = query->init(mmap->nav_mesh, 1024);
            if (dtStatusFailed(dtResult)) {
                dtFreeNavMeshQuery(query);
                cftf::loger::SysLog::Instance().Error(std::format("MMAP:GetNavMeshQuery: Failed to initialize dtNavMeshQuery for mapId {:03} instanceId {}", mapId, instanceId));
                return nullptr;
            }

            cftf::loger::SysLog::Instance().Deubg(std::format("MMAP:GetNavMeshQuery: created dtNavMeshQuery for mapId {:03} instanceId {}", mapId, instanceId));
            mmap->navmesh_queries.insert(std::pair<unsigned int, dtNavMeshQuery*>(instanceId, query));
        }

        return mmap->navmesh_queries[instanceId];
    }

}

